﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace LambdaManAi
{
    public delegate object UserFunction(object[] args);

    public class UserFunctions : LowLevelFunctions
    {
        //Args for the step
        private const int AiState = 0;
        private const int WorldState = 1;

        //4-tuple world
        private const int MapStatus = 0;
        private const int ManStatus = 1;
        private const int GhostSatatus = 2;
        private const int FruitSatus = 3;
        
        //Map cells
        private const int Wall = 0;
        private const int EmptySpace = 1;
        private const int Pill = 2;
        private const int PowerPill = 3;
        private const int Fruit = 4;
        private const int ManStartPosition = 5;
        private const int GhostStartPosition = 6;
        
        //Directions
        private const int MoveUp = 0;
        private const int MoveRight = 1;
        private const int MoveDown = 2;
        private const int MoveLeft = 3;

        //Man 5-tuple, ghost 3-tuple
        private const int Vitality = 0;
        private const int Location = 1;
        private const int Direction = 2;
        private const int Lives = 3;
        private const int Score = 4;

        //Ghost Vitality
        private const int Standard = 0;
        private const int Fright = 1;
        private const int Invisible = 2;

        //Cells price
        private const int PillPriсe = 10;        
        private const int PowerPillPriсe = 50;
        private const int FruitPriсe = 1000;


        #region Auxiliary code, don't modify it!

        public static IProcessor Processor { get; set; }

        private static object Code(Expression<Func<object>> expression, [CallerMemberName]string functionName = "")
        {
            return Processor.ProcessCode(functionName, expression);
        }

        private static object Code(Locals locals, Expression<Func<object>> expression, [CallerMemberName]string functionName = "")
        {
            return Processor.ProcessCode(functionName, locals, expression);
        }

        #endregion

        public static object MainFunction(params object[] args)
        {
            return Code(() =>
                Pair(1, Closure(StepFunction))
            );
        }

        public static object StepFunction(params object[] args)
        {
            // arguments
            const int stepState = 0;
            const int worldState = 1;

            Locals locals = new Locals(args);

            return Code(locals, () => Pair(1, GetDesiredDirection(locals[worldState])));
        }

        public static object GetDesiredDirection(params object[] args)
        {
            //args
            const int worldState = 0;

            // locals (should have numbers just after arguments!)
            const int map = 1;
            const int man = 2;
            const int manY = 3;
            const int manX = 4;
            const int curcell = 5;
            const int upcell = 6;
            const int rightcell = 7;
            const int downcell = 8;
            const int leftcell = 9;

            Locals locals = null;
            locals = new Locals(args)
            {
                {map, () => GetItem(locals[worldState], MapStatus)},
                {man, () => GetItem(locals[worldState], ManStatus)},
                {manY, () => GetItem(GetItem(locals[man], 1), 1)},
                {manX, () => GetItem(GetItem(locals[man], 1), 0)},
                {curcell, () =>NormalizeCell(GetCellValue(locals[map], locals[manY], locals[manX]))},
                {upcell, () => (int)locals[curcell] == Wall ? Wall : NormalizeCell(GetCellValue(locals[map], (int)locals[manY]-1, locals[manX]))},
                {rightcell, () =>(int)locals[curcell] == Wall ? Wall : NormalizeCell(GetCellValue(locals[map], locals[manY], (int)locals[manX]+1))},
                {downcell, () =>(int)locals[curcell] == Wall ? Wall : NormalizeCell(GetCellValue(locals[map], (int)locals[manY]+1, locals[manX]))},
                {leftcell, () =>(int)locals[curcell] == Wall ? Wall : NormalizeCell(GetCellValue(locals[map], locals[manY], (int)locals[manX]-1))}
            };

            // TODO: Select desired direction using visitedCells list filled and returned by BfsStep!!!.
            return Code(locals, () => 
                BfsStep(locals[map], 
                             List(), 
                             Enqueue(CreateEmptyQueue(), CellInfo(locals[manY], locals[manX], 0, CellInfo(-1, -1, -1, -1))))

            );
        }

        public static object BfsStep(params object[] args)
        {
            //args
            const int map = 0;
            const int visitedCells = 1;
            const int queue = 2;

            // locals (should have numbers just after arguments!)
            const int currentCellInfo = 3;

            Locals locals = null;
            locals = new Locals(args)
            {
                {currentCellInfo, () => GetFirstQueueItem(locals[queue])},
            };

            return Code(locals, () => 
                IsQueueEmpty(locals[queue])
                    ? locals[visitedCells]  // answer
                    : Contains(locals[visitedCells], currentCellInfo)   // currentCellInfo should be calculated here!!!
                        ? locals[visitedCells]  // answer
                        : BfsStep(
                            locals[map],
                            AddToList(locals[visitedCells], locals[currentCellInfo]),
                            FoldList(
                                FilterList(
                                    List(upChild, rightChild, downChild, leftChild), 
                                    (child) => (int)GetCellValue(child) != Wall
                                 ),
                                Enqueue,
                                Dequeue(locals[queue])
                            )
                          )

            );
        }

        public static object CellInfo(params object[] args)
        {
            //args
            const int y = 0;
            const int x = 1;
            const int front = 2;
            const int parentCellInfo = 3;

            // locals (should have numbers just after arguments!)

            Locals locals = new Locals(args);

            return Code(locals, () => 
                Tuple(locals[y], locals[x], locals[front], locals[parentCellInfo])
            );
        }


       


        public static object NormalizeCell(params object[] args)
        {
            const int cell = 0;
            var locals = new Locals(args);
            return Code(() =>
                (int)locals[cell] >= ManStartPosition ? EmptySpace : locals[cell]
                );
        }

        public static object Max3(params object[] args)
        {
            var locals = new Locals(args);
            return Code(() =>
                Max2(locals[0], Max2(locals[1], locals[2]))
            );
        }

        public static object Max4(params object[] args)
        {
            var locals = new Locals(args);
            return Code(() =>
                Max2(locals[0], Max2(locals[1], Max2(locals[2], locals[3])))
            );
        }

        public static object Max4Index(params object[] args)
        {
            const int max = 4;
            Locals locals = null;
            locals = new Locals(args)
                {
                   {max, () => Max4(locals[0], locals[1], locals[2], locals[3])} 
                };
            return Code(locals, () =>
                Select((int)locals[max] == (int)locals[0], 0,
                Select((int)locals[max] == (int)locals[1], 1,
                Select((int)locals[max] == (int)locals[2], 2, 3)))
            );
        }

        public static object GetCellValue(params object[] args)
        {
            const int map = 0;
            const int y = 1;
            const int x = 2;
            var locals = new Locals(args);
            return Code(() =>
                GetItem(GetItem(locals[map], (int)locals[y]), (int)locals[x])
            );
        }
    }
}